package stcu;

import com.nvlled.route.*;
import stcu.robjects.*;
import java.util.*;
import java.net.*;
import java.io.*;
import java.nio.file.*;
import java.util.concurrent.*;
import static java.nio.file.StandardWatchEventKinds.*;

public class FileManager {

    private ObjectRouter router;
    private Group group;
    private String folderPath;
    private BlockingQueue<Listener> listeners;
    private FileMonitor fileMon;
    private FileServer fileServer;

    private List<FileInfo> files;

    public FileManager(ObjectRouter router, Group group, String path) throws IOException {
        this.router = router;
        this.group = group;
        files = new ArrayList<FileInfo>();
        folderPath = path;
        listeners = new LinkedBlockingQueue<Listener>();
        group.addListener(new MemberWatch());
        fileServer = new FileServer();
        fileMon = new FileMonitor(path);

        new Thread(fileMon).start();
        new Thread(fileServer).start();

        subscribe();
    }

    public void stop() {
        fileMon.stop();
        fileServer.stop();
    }

    public FileManager(ObjectRouter router, Group group) throws IOException {
        this(router, group, ".");
    }

    public void addListener(Listener listener) {
        if (listener != null) {
            listeners.add(listener);
        }
    }

    private void subscribe() {
        router.subscribe(FileList.class, new ObjectReceiver<FileList>() {
            public void receive(FileList request) {
                System.out.println("File request received from " + request.getSourceAddress());
                for (FileInfo file: getLocalFiles()) {
                    router.deliver(file.toFileAdd(), request.getSourceAddress());
                }
            }
        });
        router.subscribe(FileAdd.class, new ObjectReceiver<FileAdd>() {
            public void receive(FileAdd request) {
                FileInfo file = new FileInfo(request);
                addFile(file);
            }
        });
        router.subscribe(FileDel.class, new ObjectReceiver<FileDel>() {
            public void receive(FileDel request) {
                FileInfo file = new FileInfo(request);
                removeFile(file);
            }
        });
        router.subscribe(FileClear.class, new ObjectReceiver<FileClear>() {
            public void receive(FileClear request) {
                clearFiles(request.getOwner());
            }
        });
    }

    private synchronized void addFile(FileInfo file) {
        if (files.contains(file)) {
            return;
        }

        files.add(file);
        for (Listener listener: listeners) {
            listener.fileAdded(file);
        }
        System.out.println("File added: " + file.getName());
    }

    private synchronized void removeFile(FileInfo file) {
        files.remove(file);
        for (Listener listener: listeners) {
            listener.fileRemoved(file);
        }
        System.out.println("File removed: " + file.getName());
    }

    public List<FileInfo> getFiles() {
        // This may actually be quite expensive.
        return new ArrayList<FileInfo>(files);
    }

    public List<FileInfo> getLocalFiles() {
        List<FileInfo> localFiles = new ArrayList<FileInfo>();
        InetSocketAddress address  = getLocalAddress();
        for (String filename: new File(folderPath).list()) {
            localFiles.add(new FileInfo(filename, 
                        group.getMemberId(),
                        address));
        }
        return localFiles;
    }

    public InetSocketAddress getLocalAddress() {
        InetAddress localAddress = router.getLocalInetAddress();
        return new InetSocketAddress(localAddress, fileServer.getLocalPort());
    }

    public String getFolderPath() {
        return folderPath;
    }

    public void setFolder(String path) throws IOException {
        if (!folderPath.equals(path)) {
            folderPath = path;
            fileMon.setDir(path);


            new Thread() {public void run() {
                String id = group.getMemberId();
                router.deliver(new FileClear(id), group.getMembers());
                Util.sleep(2);

                for (FileInfo file: getLocalFiles()) {
                    router.deliver(file.toFileAdd(), group.getMembers());
                }
            }}.start();
        }
    }

    public void clearFiles(String member) {
        for (FileInfo file: getFiles()) {
            if (file.getOwner().equals(member)) {
                removeFile(file);
            }
        }
    }

    public InputStream download(FileInfo file) throws IOException {
        Socket socket = new Socket(file.getAddress(), file.getPort());
        OutputStream out = socket.getOutputStream();
        String data = file.getName() + "\n";
        out.write(data.getBytes());
        out.flush();
        return socket.getInputStream();
    }

    public void listFiles() {
        router.deliver(new FileList(), group.getMembers());
    }

    public void listFiles(String member) {
        router.deliver(new FileList(), member);
    }

    public void listFiles(SocketAddress address) {
        router.deliver(new FileList(), address);
    }

    public void getFile(String filename) {
    }

    public static void main(String[] args) throws IOException {
        //FileMonitor filemon = new FileMonitor("tempdir");
        //System.out.println("Files in directory");
        //for (String filename: new File("tempdir").list()) {
        //    System.out.println("-> " + filename);
        //}
        //filemon.monitor();
    }

    private class FileMonitor implements Runnable {
        WatchService watcher;
        WatchKey watchKey;
        private boolean monitoring = false;

        public FileMonitor(String dir) throws IOException {
            watcher = FileSystems.getDefault().newWatchService();
            setDir(dir);
        }

        public void setDir(String dir) throws IOException {
            Path path = Paths.get(dir);
            if (watchKey != null) {
                watchKey.cancel();
            }
            watchKey = path.register(watcher, ENTRY_CREATE, ENTRY_DELETE);
        }

        public void stop() {
            monitoring = false;
            try {
                if (watchKey != null) {
                    watchKey.cancel();
                }
            } catch (Exception e) { }
        }

        public void monitor() {
            monitoring = true;

            while(monitoring) {

                WatchKey key = null;
                try {
                    key = watcher.take();
                } catch (Exception e) {
                    System.out.println("interrupted");
                    continue;
                }
                System.out.println("key taken");

                for (WatchEvent<?> event: key.pollEvents()) {
                    WatchEvent.Kind kind = event.kind();

                    if (kind == OVERFLOW) {
                        continue;
                    }

                    Path name = ((WatchEvent<Path>) event).context();
                    handleChange(name, kind);
                }
                
                key.reset();
            }
        }

        private void handleChange(Path name, WatchEvent.Kind kind) {
            if (kind == ENTRY_CREATE) {
                InetSocketAddress address = getLocalAddress();
                FileInfo file = new FileInfo(name.toString(), 
                        group.getMemberId(),
                        address);
                router.deliver(file.toFileAdd(), address);
                    
                //addFile(new FileInfo(name.toString(), 
                //            group.getMemberId(),
                //            getLocalAddress()));
                System.out.println(">File created: " + new File(name.toString()));
            } else if (kind == ENTRY_DELETE) {
                removeFile(new FileInfo(name.toString(), 
                            group.getMemberId(),
                            getLocalAddress()));
                System.out.println(">File deleted: " + name);
            } else {
                System.out.println(">File " + kind + ": " + name);
            }
        }



        public void run() {
            monitor();
        }
    }

    class MemberWatch implements Group.Listener {

        public void memberJoined(MemberAdd member){
            System.out.println("File manager memberJoined: " + member.getName() + " from " + 
                    member.getAddress());
            Util.sleep(3);
            listFiles(member.getAddress());
        }

        public void memberLeft(MemberDel member){
            System.out.println("File manager memberLeft: " + member.getName());
            Util.sleep(3);
            clearFiles(member.getIDNumber());
        }

        public void messageReceived(GroupMessage message){ }
    }

    private class FileServer implements Runnable {
        private ServerSocket serverSock; 
        private boolean running = false;

        public FileServer() {
            try {
                serverSock = new ServerSocket(0);
                System.out.println("Fileserver listening at " + 
                        serverSock.getLocalSocketAddress());

            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        public int getLocalPort() {
            return serverSock.getLocalPort();
        }

        public void stop() {
            running = false;
            try {
                serverSock.close();
            } catch (Exception e) { }
        }

        public void run() {
            running = true;
            while(running) {
                try {
                    Socket socket = serverSock.accept();
                    System.out.println("Connection accepted: " + socket);
                    new Thread(new ConnectionHandler(socket)).start();
                } catch (Exception e) { }
            }
        }
    }

    private class ConnectionHandler implements Runnable {
        private Socket socket;
        static final int BUFFER_SIZE = 8192;

        public ConnectionHandler(Socket socket) {
            this.socket = socket;
        }

        public void run() {
            try {
                InputStream in = socket.getInputStream();
                BufferedReader reader = 
                    new BufferedReader(new InputStreamReader(in));
                String filename = reader.readLine(); // should be bounded
                System.out.println("Download request for " + filename);


                if (!fileExists(filename)) {
                    socket.close();
                    return;
                }

                InputStream fileInput 
                    = new FileInputStream(new File(folderPath + "/" + filename));
                //BufferedReader fileReader = 
                    //new BufferedReader(new InputStreamReader(fileInput));

                OutputStream out = socket.getOutputStream();
                //BufferedWriter writer = 
                    //new BufferedWriter(new OutputStreamWriter(out));

                transferFile(fileInput, out);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        private void transferFile(InputStream in, OutputStream out) throws IOException {
            int count;
            byte[] buffer = new byte[BUFFER_SIZE];
            while ((count = in.read(buffer)) > 0)
            {
              out.write(buffer, 0, count);
              out.flush();
            }
            in.close();
            out.close();
            socket.close();
        }

        private boolean fileExists(String filename) {
            for (FileInfo file: getLocalFiles()) {
                if (file.getName().equals(filename)) {
                    return true;
                }
            }
            return false;
        }
    }


    public static interface Listener {
        public void fileAdded(FileInfo file);

        public void fileRemoved(FileInfo file);
    }

    private class Deaf implements Listener {
        public void fileAdded(FileInfo file) { }

        public void fileRemoved(FileInfo file) { }
    }
}

